package org.checkerframework.framework.stub;

import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Options;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.checkerframework.checker.mustcall.qual.MustCallUnknown;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.SystemUtil;
import org.checkerframework.javacutil.TypesUtils;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.StringsPlume;

/**
 * Generates a stub file from a single class or an entire package.
 *
 * <p>TODO: StubGenerator needs to be reimplemented, because it no longer works due to changes in
 * JDK 9.
 *
 * @checker_framework.manual #stub Using stub classes
 */
public class StubGenerator {
  /** The indentation for the class. */
  private static final String INDENTION = "    ";

  /** The output stream. */
  private final PrintStream out;

  /** the current indentation for the line being processed. */
  private String currentIndention = "";

  /** the package of the class being processed. */
  private String currentPackage = null;

  /** Constructs a {@code StubGenerator} that outputs to {@code System.out}. */
  public StubGenerator() {
    this(System.out);
  }

  /**
   * Constructs a {@code StubGenerator} that outputs to the provided output stream.
   *
   * @param out the output stream
   */
  public StubGenerator(PrintStream out) {
    this.out = out;
  }

  /**
   * Constructs a {@code StubGenerator} that outputs to the provided output stream.
   *
   * @param out the output stream
   */
  public StubGenerator(OutputStream out) {
    this.out = new PrintStream(out);
  }

  /** Generate the stub file for all the classes within the provided package. */
  public void stubFromField(Element elt) {
    if (!(elt.getKind() == ElementKind.FIELD)) {
      return;
    }

    String pkg = ElementUtils.getQualifiedName(ElementUtils.enclosingPackage(elt));
    if (!"".equals(pkg)) {
      currentPackage = pkg;
      currentIndention = "    ";
      indent();
    }
    VariableElement field = (VariableElement) elt;
    printFieldDecl(field);
  }

  /** Generate the stub file for all the classes within the provided package. */
  public void stubFromPackage(PackageElement packageElement) {
    currentPackage = packageElement.getQualifiedName().toString();

    indent();
    out.print("package ");
    out.print(currentPackage);
    out.println(";");

    for (TypeElement element : ElementFilter.typesIn(packageElement.getEnclosedElements())) {
      if (isPublicOrProtected(element)) {
        out.println();
        printClass(element);
      }
    }
  }

  /**
   * Generate the stub file for all the classes within the package that contains {@code elt}.
   *
   * @param elt a method or constructor; generate stub files for its package
   */
  public void stubFromMethod(ExecutableElement elt) {
    if (!(elt.getKind() == ElementKind.CONSTRUCTOR || elt.getKind() == ElementKind.METHOD)) {
      return;
    }

    String newPackage = ElementUtils.getQualifiedName(ElementUtils.enclosingPackage(elt));
    if (!newPackage.equals("")) {
      currentPackage = newPackage;
      currentIndention = "    ";
      indent();
    }

    printMethodDecl(elt);
  }

  /** Generate the stub file for provided class. The generated file includes the package name. */
  public void stubFromType(TypeElement typeElement) {

    // only output stub for classes or interfaces.  not enums
    if (typeElement.getKind() != ElementKind.CLASS
        && typeElement.getKind() != ElementKind.INTERFACE) {
      return;
    }

    String newPackageName =
        ElementUtils.getQualifiedName(ElementUtils.enclosingPackage(typeElement));
    boolean newPackage = !newPackageName.equals(currentPackage);
    currentPackage = newPackageName;

    if (newPackage) {
      indent();

      out.print("package ");
      out.print(currentPackage);
      out.println(";");
      out.println();
    }
    String fullClassName = ElementUtils.getQualifiedClassName(typeElement).toString();

    String className =
        fullClassName.substring(
            fullClassName.indexOf(currentPackage)
                + currentPackage.length()
                // +1 because currentPackage doesn't include
                // the . between the package name and the classname
                + 1);

    int index = className.lastIndexOf('.');
    if (index == -1) {
      printClass(typeElement);
    } else {
      String outer = className.substring(0, index);
      printClass(typeElement, outer.replace('.', '$'));
    }
  }

  /** helper method that outputs the index for the provided class. */
  private void printClass(TypeElement typeElement) {
    printClass(typeElement, null);
  }

  /**
   * Helper method that prints the stub file for the provided class.
   *
   * @param typeElement the class to output
   * @param outerClass the outer class of the class, or null if {@code typeElement} is a top-level
   *     class
   */
  private void printClass(TypeElement typeElement, @Nullable String outerClass) {
    indent();

    List<? extends AnnotationMirror> teannos = typeElement.getAnnotationMirrors();
    if (teannos != null && !teannos.isEmpty()) {
      for (AnnotationMirror am : teannos) {
        out.println(am);
      }
    }

    // This could be a `switch` statement.
    if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
      out.print("@interface");
    } else if (typeElement.getKind() == ElementKind.ENUM) {
      out.print("enum");
    } else if (typeElement.getKind() == ElementKind.INTERFACE) {
      out.print("interface");
    } else if (typeElement.getKind().name().equals("RECORD")) {
      out.print("record");
    } else if (typeElement.getKind() == ElementKind.CLASS) {
      out.print("class");
    } else {
      // Shouldn't this throw an exception?
      return;
    }

    out.print(' ');
    if (outerClass != null) {
      out.print(outerClass + "$");
    }
    out.print(typeElement.getSimpleName());

    // Type parameters
    if (!typeElement.getTypeParameters().isEmpty()) {
      out.print('<');
      out.print(formatList(typeElement.getTypeParameters()));
      out.print('>');
    }

    // Extends
    if (typeElement.getSuperclass().getKind() != TypeKind.NONE
        && !TypesUtils.isObject(typeElement.getSuperclass())) {
      out.print(" extends ");
      out.print(formatType(typeElement.getSuperclass()));
    }

    // implements
    if (!typeElement.getInterfaces().isEmpty()) {
      boolean isInterface = typeElement.getKind() == ElementKind.INTERFACE;
      out.print(isInterface ? " extends " : " implements ");
      List<String> ls =
          CollectionsPlume.mapList(StubGenerator::formatType, typeElement.getInterfaces());
      out.print(formatList(ls));
    }

    out.println(" {");
    String tempIndention = currentIndention;

    currentIndention = currentIndention + INDENTION;

    // Inner classes, which the stub generator prints later.
    List<TypeElement> innerClass = new ArrayList<>();
    // side-effects innerClass
    printTypeMembers(typeElement.getEnclosedElements(), innerClass);

    currentIndention = tempIndention;
    indent();
    out.println("}");

    for (TypeElement element : innerClass) {
      printClass(element, typeElement.getSimpleName().toString());
    }
  }

  /**
   * Helper method that outputs the public or protected inner members of a class.
   *
   * @param members list of the class members
   */
  private void printTypeMembers(List<? extends Element> members, List<TypeElement> innerClass) {
    for (Element element : members) {
      if (isPublicOrProtected(element)) {
        printMember(element, innerClass);
      }
    }
  }

  /** Helper method that outputs the declaration of the member. */
  private void printMember(Element member, List<TypeElement> innerClass) {
    if (member.getKind().isField()) {
      printFieldDecl((VariableElement) member);
    } else if (member instanceof ExecutableElement) {
      printMethodDecl((ExecutableElement) member);
    } else if (member instanceof TypeElement) {
      innerClass.add((TypeElement) member);
    }
  }

  /**
   * Helper method that outputs the field declaration for the given field.
   *
   * <p>It indicates whether the field is {@code protected}.
   */
  private void printFieldDecl(VariableElement field) {
    if ("class".equals(field.getSimpleName().toString())) {
      error("Cannot write class literals in stub files.");
      return;
    }

    indent();

    List<? extends AnnotationMirror> veannos = field.getAnnotationMirrors();
    if (veannos != null && !veannos.isEmpty()) {
      for (AnnotationMirror am : veannos) {
        out.println(am);
      }
    }

    // if protected, indicate that, but not public
    if (field.getModifiers().contains(Modifier.PROTECTED)) {
      out.print("protected ");
    }
    if (field.getModifiers().contains(Modifier.STATIC)) {
      out.print("static ");
    }
    if (field.getModifiers().contains(Modifier.FINAL)) {
      out.print("final ");
    }

    out.print(formatType(field.asType()));

    out.print(" ");
    out.print(field.getSimpleName());
    out.println(';');
  }

  /**
   * Helper method that outputs the method declaration for the given method.
   *
   * <p>IT indicates whether the field is {@code protected}.
   */
  private void printMethodDecl(ExecutableElement method) {
    indent();

    List<? extends AnnotationMirror> eeannos = method.getAnnotationMirrors();
    if (eeannos != null && !eeannos.isEmpty()) {
      for (AnnotationMirror am : eeannos) {
        out.println(am);
      }
    }

    // if protected, indicate that, but not public
    if (method.getModifiers().contains(Modifier.PROTECTED)) {
      out.print("protected ");
    }
    if (method.getModifiers().contains(Modifier.STATIC)) {
      out.print("static ");
    }

    // print Generic arguments
    if (!method.getTypeParameters().isEmpty()) {
      out.print('<');
      out.print(formatList(method.getTypeParameters()));
      out.print("> ");
    }

    // not return type for constructors
    if (method.getKind() != ElementKind.CONSTRUCTOR) {
      out.print(formatType(method.getReturnType()));
      out.print(" ");
      out.print(method.getSimpleName());
    } else {
      out.print(method.getEnclosingElement().getSimpleName());
    }

    out.print('(');

    boolean isFirst = true;
    for (VariableElement param : method.getParameters()) {
      if (!isFirst) {
        out.print(", ");
      }
      out.print(formatType(param.asType()));
      out.print(' ');
      out.print(param.getSimpleName());
      isFirst = false;
    }

    out.print(')');

    if (!method.getThrownTypes().isEmpty()) {
      out.print(" throws ");
      List<String> ltt =
          CollectionsPlume.mapList(StubGenerator::formatType, method.getThrownTypes());
      out.print(formatList(ltt));
    }
    out.println(';');
  }

  /** Indent the current line. */
  private void indent() {
    out.print(currentIndention);
  }

  /**
   * Returns a string representation of the list in the form of {@code item1, item2, item3, ...},
   * without surrounding square brackets as the default representation has.
   *
   * @param lst a list to format
   * @return a string representation of the list, without surrounding square brackets
   */
  private String formatList(@MustCallUnknown List<? extends @MustCallUnknown Object> lst) {
    return StringsPlume.join(", ", lst);
  }

  /** Returns true if the element is public or protected element. */
  private boolean isPublicOrProtected(Element element) {
    return element.getModifiers().contains(Modifier.PUBLIC)
        || element.getModifiers().contains(Modifier.PROTECTED);
  }

  /**
   * Returns the simple name of the type.
   *
   * @param typeRep a type
   * @return the simple name of the type
   */
  private static String formatType(TypeMirror typeRep) {
    StringTokenizer tokenizer = new StringTokenizer(typeRep.toString(), "()<>[], ", true);
    StringBuilder sb = new StringBuilder();

    while (tokenizer.hasMoreTokens()) {
      String token = tokenizer.nextToken();
      if (token.length() == 1 || token.lastIndexOf('.') == -1) {
        sb.append(token);
      } else {
        int index = token.lastIndexOf('.');
        sb.append(token.substring(index + 1));
      }
    }
    return sb.toString();
  }

  /**
   * The main entry point to StubGenerator.
   *
   * @param args command-line arguments
   */
  @SuppressWarnings("signature") // User-supplied arguments to main
  public static void main(String[] args) {
    if (args.length != 1) {
      System.out.println("Usage:");
      System.out.println("    java StubGenerator [class or package name]");
      return;
    }

    Context context = new Context();
    Options options = Options.instance(context);
    if (SystemUtil.jreVersion == 8) {
      options.put(Option.SOURCE, "8");
      options.put(Option.TARGET, "8");
    }

    JavaCompiler javac = JavaCompiler.instance(context);
    javac.initModules(com.sun.tools.javac.util.List.nil());
    javac.enterDone();

    ProcessingEnvironment env = JavacProcessingEnvironment.instance(context);

    StubGenerator generator = new StubGenerator();

    if (env.getElementUtils().getPackageElement(args[0]) != null) {
      generator.stubFromPackage(env.getElementUtils().getPackageElement(args[0]));
    } else if (env.getElementUtils().getTypeElement(args[0]) != null) {
      generator.stubFromType(env.getElementUtils().getTypeElement(args[0]));
    } else {
      error("Couldn't find a package or a class named " + args[0]);
    }
  }

  private static void error(String string) {
    System.err.println("StubGenerator: " + string);
  }
}
